<?php
/**
 * 功能作用: 字符串相似度处理
 * 思路解析
 * 1. 将需要处理的两个字符串数据以参数形式传入
 * 2. 调用 $this->splitString(数组参数) 函数进行字符串转数组处理
 *    如传入的数据内容为:
 *      $str = '你好123吗' 转成 array('你','好','1','2','3','吗')
 * 3. 统计两个字符串分割成数组后的交集,如:
 *      $str1 = '你好', $str2 = '你好吗',则字数交集数量: (0=>'你',1=>'好') 即为2个
 * 4. 递归计算两个字符串分割成数组后的各数组元素相加之和
 * 5. 计算两个字符串的相似度
 *    相似度 = （ 获取两个字符串分割成数组后的交集 * 需要匹配处理的字符串条数 ） / 递归计算两个字符串分割成数组后的各数组元素相加之和
 */

/**
 * 按文本字符串、按文件相似度处理类
 * by 周文晓 2017年03月27日
 */
class Similar {

    // 默认设置字符集
    protected $charset = 'UTF-8';
    // 保存分割字符串之后生成的数组
    protected $arr_strs = array();
    // 文件匹配相似度排序规则
    protected $sort_type = SORT_DESC;

    /**
     * 将字符串分割成每个字符为一个单元数组
     * @param array $arr_str 需要处理的字符串数据,默认以数组传入
     * @return bool
     */
    public function splitString($arr_str = array()) {
        // 如果需要比对的字符串小于两个,则不予处理
        if (count($arr_str) < 2)
            return false;
        foreach ($arr_str as $k => $v) {
            if (empty($v))
                continue;
            // 分割字符串,格式如: array('你','好','a','1')
            $this->arr_strs[$k] = $this->filterEmptyArray($this->mbstringToArray($v));
        }
        //最终生成的字符串数组小于2个，则不予处理
        return count($this->arr_strs) > 1 ? true : false;
    }

    /**
     * 将字符串分割成数组单元,字符串拆分
     * @param $str 需要处理的字符串
     * @param String $charset 处理所需要的字符集
     * @return Array 格式如: array('你','好','a','1')
     */
    protected function mbstringToArray($str, $charset = '') {
        if (empty($charset))
            $charset = $this->charset;
        $strlen = mb_strlen($str);
        while ($strlen) {
            $splice_str = mb_substr($str, 0, 1, $charset);
            $data[] = $splice_str;
            $str = mb_substr($str, 1, $strlen, $charset);
            $strlen = mb_strlen($str);
        }
        return $data;
    }

    /**
     * 过滤数组内所有的空值数据
     * @param array $arr 需要过滤处理的数组数据
     * @return Array
     */
    protected function filterEmptyArray($arr = array()) {
        $data = array();
        foreach ($arr as $v) {
            if ($v == ' ' || empty($v))
                continue;
            $data[] = $v;
        }
        return $data;
    }

    /**
     * 按两个字符串匹配相似度
     * @param $is_persent 所需要的数据类型 true表示返回百分比(默认) false代表返回小数
     */
    public function getSimilarity($arr_strs, $is_persent = true) {
        // 分割字符串处理
        $this->splitString($arr_strs);
        // 步骤(1). 获取两个字符串分割成数组后的交集
        $override_count = count(array_intersect($this->arr_strs[0], $this->arr_strs[1]));
        // 步骤(2). 递归计算两个字符串分割成数组后的各数组元素相加之和
        $total_count = count(array_merge_recursive($this->arr_strs[0], $this->arr_strs[1]));
        if($override_count == 0)
            $calc_similarity = 0;
        else
            // 计算相似度,相似度 = 步骤(1) * 需要处理的字符串条数 / 步骤(2)
            $calc_similarity = $override_count * count($this->arr_strs) / $total_count;
        // 是否以百分比输出数据
        if ($is_persent)
            $calc_similarity = round($calc_similarity * 100, 2);
        return $calc_similarity;
    }

    /**
     * 将数据以表格形式输出到页面用于浏览
     * @param $arr 需要展示到页面的数组集合
     */
    public function showByTable($arr) {
        $str = '';
        $str .= '<table>
                    <tr>
                        <th>行号</th>
                        <th>基文件</th>
                        <th>需匹配文件</th>
                        <th>基文件内容</th>
                        <th>需匹配文件内容</th>
                        <th>匹配度</th>
                    </tr>';
        foreach($arr as $v) {
            $str .= "<tr>
                        <td>{$v['line']}</td><td>{$v['base_file']}</td>
                        <td>{$v['pro_file']}</td><td>{$v['base_text']}</td>
                        <td>{$v['pro_text']}</td><td>{$v['similar']}%</td>
                    </tr>";
        }
        $str .= '</table>';
        return $str;
    }

    /**
     * 按两个文件匹配相似度
     * @param Array $files_arr 两个需要匹配的文件组成的数组参数
     * @param Integer $sort_type 排序规则 SORT_DESC降序 SORT_ASC升序
     * @return Array
     */
    public function filePro($files_arr = array(), $sort_type=SORT_DESC) {
        // 判断传入的有效参数是否为两个匹配文件
        $file_count = count($files_arr);
        if($file_count  < 2)
            exit('请传入两个匹配文件对应的路径地址');
        // 循环按行读取内容
        for($i = 0; $i < $file_count; $i++) {
            if( ! is_readable($files_arr[$i]) ||  ! file_exists($files_arr[$i])) {
                exit('[ERROR] FILE PATH: "' . $files_arr[$i] . '" 文件不可读或不存在');
            }
            // 循环打开文件生成资源
            $fh[$i] = @fopen($files_arr[$i],"r");
        }
        $desc_arr = array();
        while (! feof($fh[0])) {
            // 将字文本的回车换行转为空字符串
            $first_fh_row = str_replace(PHP_EOL,'',fgets($fh[0]));
            // 将文件指针移动到文件开头
            rewind($fh[1]);
            // 生成行号用于打印输出显示
            $lines = 1;
            while (! feof($fh[1])) {
                $second_fh_row = str_replace(PHP_EOL,'',fgets($fh[1]));
                $cal_similar = $this->getSimilarity(array($first_fh_row, $second_fh_row));
                $desc_arr[] = array(
                    'similar' => $cal_similar,     // 相似度
                    'line' => $lines,              // 行号
                    'base_file' => $files_arr[0],  // 基文件
                    'pro_file' => $files_arr[1],   // 需要匹配处理的文件
                    'base_text' => $first_fh_row,  // 基文件对应的内容
                    'pro_text' => $second_fh_row   // 需要匹配处理的文件内容
                );
                $lines++;
            }
        }
        if(! in_array($sort_type, array(SORT_DESC,SORT_ASC)))
            $sort_type = $this->sort_type;
        foreach ($desc_arr as $v) {
            $similar[] = $v['similar'];
        }
        // 按$sort_desc进行排序并返回结果集
        array_multisort($similar , $sort_type, $desc_arr);
        return $desc_arr;
    }
}
